  // 学习参考文档：https://juejin.cn/post/7021672733213720613#heading-9
  // 正则表达式是匹配模式，要么匹配字符，要么匹配位置
  console.log('匹配位置 =======> start')
  console.log(
    '正则表达式中常用的匹配位置符号有： ^、$、\\b、\\B、?=p、(?!p)、(?<=p)、(?<!p)',
  )
  // ^ 匹配头位置
  let location1 = 'hello'
  console.log('在头部位置添加一个😄 ==> ', location1.replace(/^/, '😄'))
  // $ 匹配尾位置
  let location2 = 'hello'
  console.log('在尾部位置添加一个😄 ==> ', location2.replace(/$/, '😄'))
  // \b 单词的边界 区分单词
  let location3 = 'admin.user.html'      
  console.log(`
    \\b 匹配单词得边界
    ① \\w和\\W之间的位置 （匹配字母 和 .的位置  ab.c  也就是  ad后  和 c前面的位置）
    ② ^与\\w之间的位置 （第一个字符到 字母的位置  abc  也就是 a前面得位置）
    ③ \\w与$之间的位置 （字母到最后一个符号的位置 abc 也就是 c后面的位置）
  `)
  console.log('给每个单词两边插入😄 ==> ', location3.replace(/\b/g, '😄'))
  // \B 非单词的边界，也就是\b反着来的意思
  console.log(`
    \\B 匹配单词得边界
    ① \w与\w之间的位置 （字母和字母之间  abc  ab之间的位置 bc之间的位置）
    ② \W与\W之间的位置 （.和.的位置   .. 会匹配到中间的位置 ）
    ③ ^与\W之间的位置  （开头和.的位置）
    ④ \W与$之间的位置  （.到结尾的位置）
  `)
  let location4 = 'admin.user..html.'
  console.log('给每个单词两边插入😄 ==> ', location4.replace(/\B/g, '😄'))
  // (?=p) 前瞻 匹配符合p子模式前面的那个位置
  let location5 = 'this is 。'
  console.log('在。的前面 加上水果名称')
  let sg = ['苹果', '香蕉', '梨']
  sg.forEach((v) => {
    // 这样就可以在 。号前面加上水果名称了
    console.log(location5.replace(/(?=。)/g, v))
  })
  // (?!p) 负前瞻(也就是后面) 匹配到的位置之外的位置都是属于(?!p)的 也就是 和 (?=p) 相反, 不满足(?=p)的位置 都属于 也就是说T的前后位置都不符合
  // 通俗理解就是查找后面不是p的位置
  let location6 = 'this is TTT'
  console.log(
    '匹配除了。号以外的，在前后面加上笑脸',
    location6.replace(/(?!T)/g, '😄'),
  )
  console.log('特例 xxxx_love_study_1.mp4'.replace(/(?!xxx)/g, '❤️'))

  // (?<=p) 后顾 符合p子模式后面(注意(?=p)表示的是前面)的那个位置
  let location7 = 'this is 水果：'
  sg.forEach((v) => {
    // 这样就可以在 ：号后面加上水果名称了
    console.log(location7.replace(/(?<=：)/g, v))
  })

  // (?<!p) 负后顾  (?<=p)反过来的意思，可以理解为(?<=p)匹配到的位置之外的位置都是属于(?<!p)的
  // 例如： (?<!exp2) 查找前面不是xxx的位置
  console.log('xxxx_love_study_1.mp4'.replace(/(?<!xxx)/g, '❤️'))
  console.log('匹配位置 =======> end')
  console.log('=======================================')
  console.log('匹配位置练习题 =======> stat')
  console.log(`
  位置第一题： 将123456789转化为123,456,789
  分析： 分割成三个数字排列的 也就是说三个位置后面的 故得条件为 /d{3} 可以匹配三个数字
  答案如下： '123456789'.replace(/(?!^)(?=(\d{3})+$)/g, ',') ${'123456789'.replace(
    /(?!^)(?=(\d{3})+$)/g,
    ',',
  )}
  `)
  console.log(`
  位置第二题： 将手机号18379836654转化为183-7983-6654
  答案如下： '18379836654'.replace(/(?=(?:\d{4})+$)/g, '-') ${'18379836654'.replace(
    /(?=(?:\d{4})+$)/g,
    '-',
  )}
  `)
  console.log(`
  位置第三题： 将手机号18379836654转化为3-3-4格式的  例如用输入前四位 1234 应当变成123-4  输入12345678 应当变为 123-4567-8
  答案如下： '18379836654'.replace(/(?<=\d{3})\d+/, ($0) => '-' + $0)
      .replace(/(?<=[\d-]{8})\d{1,4}/, ($0) => '-' + $0)  ${'18379836654'
        .replace(/(?<=\d{3})\d+/, ($0) => '-' + $0)
        .replace(/(?<=[\d-]{8})\d{1,4}/, ($0) => '-' + $0)}
  `)
  console.log(`
  位置第四题： 验证密码的合法性 密码长度是6-12位，由数字、小写字符和大写字母组成，但必须至少包括2种字符
  题目解析
  题目由三个条件组成
    ① 密码长度是6-12位
    ② 由数字、小写字符和大写字母组成
    ③ 必须至少包括2种字符
  答案如下： 'isPwdTrue('ha11111A')', isPwdTrue('ha11111A')
  `)
  // 实现逻辑
  function isPwdTrue(v) {
    // 1.验证长度
    // const reg = /^[a-zA-Z\d]{6,12}$/
    // 2.验证必须包含一个数字 一个小写字母 一个大写字母
    // let reg = /((?=.*\d)(?=.*[a-z])(?=.*[A-Z]))/
    // 3.结合上面两个可以得出： 验证长度 6-12位 切包含小写字母 大写字母
    let reg = /((?=.*\d)(?=.*[a-z])(?=.*[A-Z]))^[a-zA-Z\d]{6,12}$/
    return reg.test(v)
  }
  console.log('匹配位置 =======> end')
  console.log(
    '❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️❤️',
  )
  console.log('匹配字符 =======> start')
  let strReg1 = /ab{2,5}c/g
  let strReg2 = /a[123]b/g
  console.log(`
    两种模糊匹配 横向匹配 纵向匹配
    横向: 通过量词+、*、?、{m,n}，可实现横向匹配
    例如： let strReg1 = /ab{2,5}c/g         
    strReg1.test("abc") ${runReg(strReg1, 'abc')}    
    strReg1.test("abbc") ${runReg(strReg1, 'abbc')}  
    strReg1.test("abbbbc")  ${runReg(strReg1, 'abbbbc')}  

    纵向：一个正则匹配的字符串，具体到某一位字符时，可以不是某个确定的字符串
    例如：let strReg2 = /a[123]b/g
    strReg2.test("a1b") ${runReg(strReg2, 'a1c')}    
    strReg2.test("a2b") ${runReg(strReg2, 'a1c')}    
    strReg2.test("a12b") ${runReg(strReg2, 'a1c')}    
  `)
  // 匹配一个字符 必须是范围内的
  let strReg3 = /^[0-6a-fA-F]$/
  // 匹配一个字符 不能是123
  let strReg4 = /^[^123]$/
  console.log(`
  字符组: 一组用于代表一个字符的可能性
  常用的有如下
  范围表示法: [123456abcdefABCDEF]  表示一个字符可以是规定范围下的任何一个缩写为 [0-6a-fA-F]
  strReg3.test("8")  ${runReg(strReg3, 8)}
  strReg3.test("1")  ${runReg(strReg3, 1)}
  排除字符组：有时候我们的字符取值范围会非常广 所以我们还可以指定 非X即可 只要不是X就是对的 [^123] 非1、2、3其他都是true
  strReg4.test(4)  ${runReg(strReg4, 4)}
  strReg4.test(3)  ${runReg(strReg4, 3)}

  常见的可能性字符组简写形式如下
  \\d  表示是一个数字       [^\d] 表示不能是数字   [\d] 表示只能是数字
  \\D  表示是一个非数字     [^\D] 表示必须是数字   [\D] 表示不能是数字
  \\w  表示范围[0-9a-zA-Z_]   同上                同上
  \\W  表示范围[^0-9a-zA-Z_]  同上                同上
  ......
  `)

  console.log(`
  量词 & 简写
    {m,}   至少出现m次
    {m}    出现m次
    ?      出现0次或者1次，等价于{0,1}    
    +      至少出现1次,等价于{1,} 
    *      出现一次,等价于{0,}  
  `)

  let strReg5 = /\d{2,5}/g
  let strReg6 = /\d{2,5}?/g
  let string = '123 1234 12345 123456'
  console.log(`
  贪婪匹配 VS 惰性匹配  正则本身是贪婪的，会尽可能的多匹配符合模式的字符 
  例如： 
  let string = '123 1234 12345 123456'
  let strReg5 = /\d{2,5}/g
  贪婪匹配： string.match(strReg5)  ${string.match(
    strReg5,
  )} 只会匹配到连续的2,5个数字 符合的最大基数为准
  let strReg6 = /\d{2,5}?/g
  惰性匹配 string.match(strReg6)  ${string.match(
    strReg6,
  )}   凡是符合的全部返回 最小基数2为准
  `)

  let strReg7 = /1|2|3/
  let strReg8 = /good|goodbye|nice/g
  let string2 = 'goodbye  nice try.'
  console.log(`
  多选分支： 一个模式可以实现横向和纵向的模糊匹配，而多选分支可以支持多个子模式任选其一 标识符为： |
  例如：
  let strReg7 = /1|2|3/    
  strReg7.test(1)  ${runReg(strReg7, 1)}
  strReg7.test(4)  ${runReg(strReg7, 4)}
  注意分支结构是惰性的，前面的匹配上了，后面的就不再尝试了
  例如：let strReg7 = /1|12/    
  string2.match(strReg8')  ${string2.match(strReg8)}  
  我们会发现 本来因该匹配goodBye得 但是一位内分支结构的惰性，当满足1后，会自动忽视条件2 从而继续向下验证
  `)
  let strReg9 = /id=".*"/
  let strReg10 = /id="[^"]*"/
  let string3 = '<div id="container" class="main"></div>'

  let strReg11 = /#([a-fA-F\d]{6}|[a-fA-F\d]{3})/g
  let string4 = '#ffbbad #Fc01DF #FFF #ffE'
  console.log(`
  案例分析
  1.匹配id
  let regex = /id=".*?"/    想想为什么要加? 
  解析：因为.是通配符，本身就匹配双引号的，而量词*又是贪婪的，当遇到container后面双引号时，不会停下来，会继续匹配，直到遇到最后一个双引号为止。
  let string = '<div id="container" class="main"></div>';
  方法一 ${string3.match(strReg9)}
  方法二 ${string3.match(strReg10)}
  因为方法一是 .* 表示匹配所有 直到最后一个"停止
  方法二排除了在双引号下 [^"] 也就是说 双引号下不在匹配双引号了 而是匹配非双引号的字符

  2.匹配16进制的颜色值
  let regex = /#([a-fA-F\d]{6}|[a-fA-F\d]{3})/g
  let string = "#ffbbad #Fc01DF #FFF #ffE";
  ${string4.match(strReg11)}
  `)

  // 表示 匹配ab这个分组字符至少一次 + 表示 {1,}
  let strReg12 = /(ab)+/g
  let string5 = 'ababa abbb ababab'
  let reg = /(\d{4})-(\d{2})-(\d{2})/

  console.log(`
  括号的神奇作用：括号的作用是提供了分组(括号内的正则是一个整体，即提供子表达式)，便于我们引用它
  分组, 把指定的字符装起来成为一个分组
  strReg12.test(string5)  ${strReg12.test(string5)}

  括号提取数据：'2021-08-14'.match(reg)  ${'2021-08-14'.match(reg)}
  $1 ${RegExp.$1}
  $2 ${RegExp.$2}
  $3 ${RegExp.$3}
  `)

  function runReg(reg, t) {
    if (!isRegExp(reg)) {
      let s = `${reg} is not regular expressions`
      console.error(s)
      return false
    }
    // 正则表达式不能同时校验多次, 如果需要校验多次需要设置起始位置为0
    reg.lastIndex = 0
    return reg.test(t)
  }

  // 正则传参验证
  function isRegExp(v) {
    return Object.prototype.toString.call(v) === '[object RegExp]'
  }
